今天是繼承的最後一篇,介紹另一個在物件導向程式語言常常討論的議題:繼承和組合之比(或之爭)。
物件導向領域的繼承機制,父子類別間的關係,常會用「子類別is a
父類別」來形容。以我們一直沿用的Tree為例,父類別是Tree
,子類別有Hardwood
和Conifer
,另外孫輩Timba
多重繼承自Hardwood
及Conifer
。用is a
來「造句」,就是A Hardwood is a Tree.
,A Confider is a Tree.
,A Timba is a Hardwood.
,以及A timba is a Confifer
。隔代的A Timba is a Tree.
也是成立的。這幾句英語非常容易讓我們理解類別之間的關係。
至於今天的重頭戲組合
,英文是composition
,這是名詞,動詞則為composite
。
組合是另一種類別間的關係:兩個或多個類別有相關,但並不是父子間的「垂直」血源關係,而是「擁有」或「包含」的關係,好比公司擁有員工、汽車擁有零件、樹擁有根幹枝葉等。換個詞兒可以這樣說:員工是組成公司的一部分、零件是汽車的一部分、根幹枝葉則都是樹的一部分。
在繼承體系中,子類別自動將父類別的所有屬性和方法(私有者例外)加到他自己之中。但組合則沒有這種情形,兩(或多)個類別你是你、我是我。雖然你擁有我,但我倆依然互相獨立,資源互用當然沒有問題,不過你的資源不會融入到我身上,我也不會把我的寶貝奉送給你。
也就是說,透過組合,類別之間不存在「我泥中有你,你泥中有我」的「你儂我儂」。它們是「協作」,一起完成某些事情,或者一起製造某些產品。
通常我們以「類別Ahas a
類別B」(註1)來形容組合。例如樹的主要部份是根、幹、枝、葉,我們就可以說A tree has roots.
, A tree has a trunk.
, A tree has branches.
以及A tree has leaves
。
換成中文,繼承用「是」:闊葉樹是樹、針葉樹是樹、白馬是馬...。組合則用「有」:樹有根、樹有幹、樹有枝、樹有葉、馬有蹄...。中文比英文簡潔,且不必管單複數。然而為了「國際接軌」,下文筆者還是用英文'is a'和'has a'來區別兩者。
再補充兩個術語:如果類別A has a 類別B。這時類別A通常稱為composite類別
,而類別B則叫做component類別
。
"Talk is cheap. Show me the code.":
class Tree():
def __init__(self, breed: str, age: int, height: int, root_system: str, leaf_type):
self.__breed = breed
self.__age = age
self.__height = height
self.__roots = Roots(root_system)
self.__trunk = Trunk()
self.__branches = Branches()
self.__leaves = Leaves(leaf_type)
@property
def breed(self) -> str:
return self.__breed
@property
def age(self) -> int:
return self.__age
@property
def height(self) -> int:
return self.__height
@property
def roots(self):
return self.__roots
@property
def leaves(self):
return self.__leaves
def grow(self):
...
def reproduce(self):
...
class Roots():
def __init__(self, root_system: str): # constructor
self.__root_system = root_system # three(3) types of root systems: taproot, fibrous, adventitious
@property
def root_system(self) -> str:
return self.__root_system
def absorb_water(self):
print(f'{__class__.__name__} {self.root_system} is absorbing water.')
def swarm(self):
print(f'{__class__.__name__} {self.root_system} is swarming.')
class Leaves():
def __init__(self, leaf_type: str):
self.__leaf_type = leaf_type # three(3) basic leaf types: needles, scales and broadleaf.
@property
def leaf_type(self) -> str:
return self.__leaf_type
def photosynthesize(self): # 進行光合作用。
print(f'{__class__.__name__} {self.leaf_type} is photosynthesizing.')
...
class Trunk():
...
class Branches():
...
可以這樣使用:
tree = Tree('cedar', 159, 1_900, 'fibrous', 'needles')
print(f'''
{tree.breed = :8}{tree.age = :<8,}{tree.height = :<8,}
{tree.roots.root_system = :10}{tree.leaves.leaf_type = }''')
print()
tree.roots.absorb_water()
tree.roots.swarm()
print()
tree.leaves.photosynthesize()
輸出:
也許您會說,上例的整棵樹和其根、幹、枝、葉本來就有一定關係。根、幹、枝、葉都是樹的構造成分,這個比喻不知有無曲解了組合的原義。如果有此顧慮,筆者就換一個情境好了。依然是樹,這次每棵樹上都有一間不同顏色的小木屋(樹屋),另外每棵樹都綁著數量不同、或多或少的黃絲帶。小木屋和黃絲帶本來和樹毫不相干,我們試用組合方式將這三者結合:
class Tree():
def __init__(self, breed: str, age: int, height: int, cabin_color: str='white', ribbon_amount: int=0):
self.__breed = breed
self.__age = age
self.__height = height
self.__cabin = Cabin(cabin_color)
self.__ribbon = YellowRibbon(ribbon_amount)
@property
def breed(self) -> str:
return self.__breed
@property
def age(self) -> int:
return self.__age
@property
def height(self) -> int:
return self.__height
@property
def cabin(self) -> str:
return self.__cabin
@property
def ribbon(self) -> int:
return self.__ribbon
def grow(self):
...
def reproduce(self):
...
class Cabin():
def __init__(self, color: str): # constructor
self.__color = color
@property
def color(self) -> str:
return self.__color
def provide_shelter(self):
print(f'The {self.color} cabin is providing shelter.')
class YellowRibbon():
def __init__(self, amount: int):
self.__amount = amount # 掛多少條黃絲帶。
@property
def amount(self) -> int:
return self.__amount
def forgive(self):
print(f'We have {self.amount} {__class__.__name__}(s) as a sign of forgiveness.')
測試程式:
tree = Tree('cedar', 159, 1_900, cabin_color='red', ribbon_amount=500)
print(f'''
{tree.breed=:10}{tree.age=:<10,}{tree.height=:<,}
{tree.cabin.color=:12}{tree.ribbon.amount=}''')
print()
tree.cabin.provide_shelter()
print()
tree.ribbon.forgive()
輸出:
loosely coupled
(低耦合),而繼承則為strongly coupled
(高耦合)。is a
的關係。如果不是is a
而是has a
,請使用組合。註1:如果是「一對多」的組合關係,類別B改用複數就行。